昨天我們提到,C#中的Task
是以背景執行的任務排程器透過一定的機制去輪詢(Poll)執行中的Task
狀態,進一步的介紹可以看一下TaskScheduler
的文件說明。
以昨天的while
為例,這種瘋狂詢問狀態的做法,不可避免的會產生無效的詢問(詢問的時候Task尚未完成),要讓非同步程式高效的作法一個要點,就是盡可能讓調度程式在剛剛好的時機點詢問狀態,接下來就要介紹Rust是如何作到的:
Async-Book中採取了一個化簡過後的SimpleFuture
:
trait SimpleFuture {
type Output;
fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}
enum Poll<T> {
Ready(T),
Pending,
}
另外我們來看一個簡短的程式碼:
fn main(){
example_runtime::block_on(async_method);
}
async fn async_method() -> impl SimpleFuture<()> {
todo!();
}
上面程式碼中,example_runtime
會提供一個非同步執行器executor
來處理async_method
回傳的SimpleFuture
物件,executor
會呼叫SimpleFuture
提供的poll
方法,這個行為就相當於C#中的TaskScheduler
詢問Task
的執行狀態,通常情況下這個SimpleFuture
可以大概實作成下面這樣:
pub struct TestFuture
{
status: bool
}
impl SimpleFuture for TestFuture {
type Output = ();
fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
if self.status {
Poll::Ready(())
} else {
self.status = push(wake);
Poll::Pending
}
}
}
在這個實作中,執行器調用poll時,如果future已經完成了就會回傳Ready
,如果沒有的話就會使用由執行器傳入的wake
方法來推進整個任務的進行。
wake是什麼東西呢?讓我們從C#的例子來看,當排程器在處裡一個Task
的時候,除非詢問他的狀態,否則無法知道任務是否完成,而Task
本身需要被動的等待詢問。而Future trait的設計上則會在執行器輪詢的時候拿到執行器提供的方法,也就是說當任務有進展的時候,可以透過這個開口通知執行器,主動接受執行器的輪詢。
當然,非同步程式並沒有那麼簡單,rust作為一個標榜安全性的語言,實際上的future trait是長這個樣子:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
前面也有提到Pin是用於固定記憶體位置,而Context就是前面fn wake的強化版本,可以用來管理複雜的連線狀況。
這個設計其實很巧妙的定義了future與executor的關係,讓兩者的溝通由外層直接相依於IsCompleted
變成依賴於Context
這個抽象的通道,並且透過CallBack的方式反向通知上層邏輯,讓介面兩側的元件各只要關心自己的職責-任務的管理以及任務的進行。
採用無腦輪詢方案會需要為每個任務都準備一個執行緒來監看進度,舉例來說就像是網購的郵局包裹一樣,當包裹寄出之後,購買人需要一直去郵局的網站查詢包裹的貨態,才可以安排時間在家裡等待包裹,非常不經濟。
Rust的Future設計上則是由任務主動通知執行器任務完成,有點像去咖啡店點一杯咖啡,店員會給你一個呼叫器,等到呼叫器響了再去櫃檯拿咖啡就好了,盡可能在需要的時候才去問店員咖啡做好了沒。